/*
 * Copyright (c) 2010 CommonsWare, LLC
 * Portions Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.commonsware.cwac.tlv;

import com.cloudmind.filetransfer.R;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.ListView;

public class TouchListView extends ListView {
	private ImageView mDragView;
	private WindowManager mWindowManager;
	private WindowManager.LayoutParams mWindowParams;
	private int mDragPos;      // which item is being dragged
	private int mFirstDragPos; // where was the dragged item originally
	private int mDragPoint;    // at what offset inside the item did the user grab it
	private int mCoordOffset;  // the difference between screen coordinates and coordinates in this view
	private DragListener mDragListener;
	private DropListener mDropListener;
	private RemoveListener mRemoveListener;
	private int mUpperBound;
	private int mLowerBound;
	private int mHeight;
	private GestureDetector mGestureDetector;
	public static final int FLING = 0;
	public static final int SLIDE_RIGHT = 1;
	public static final int SLIDE_LEFT = 2;
	private int mRemoveMode = -1;
	private Rect mTempRect = new Rect();
	private Bitmap mDragBitmap;
	private final int mTouchSlop;
	private int mItemHeightNormal=-1;
	private int mItemHeightExpanded=-1;
	private int grabberId=-1;
	private int dragndropBackgroundColor=0x00000000; 

	public TouchListView(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}
	
	public TouchListView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		
		mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
		
		if (attrs!=null) {
			TypedArray a=getContext().obtainStyledAttributes(attrs, R.styleable.TouchListView, 0, 0);
		
			mItemHeightNormal=a.getDimensionPixelSize(R.styleable.TouchListView_normal_height, 0);
			mItemHeightExpanded=a.getDimensionPixelSize(R.styleable.TouchListView_expanded_height, mItemHeightNormal);
			grabberId=a.getResourceId(R.styleable.TouchListView_grabber, -1);
			dragndropBackgroundColor=a.getColor(R.styleable.TouchListView_dragndrop_background, 0x00000000);
			mRemoveMode=a.getInt(R.styleable.TouchListView_remove_mode, -1);
			
			a.recycle();
		}
  }
	
	@Override
	final public void addHeaderView (View v, Object data, boolean isSelectable) {
		throw new RuntimeException("Headers are not supported with TouchListView");
	}
	
	@Override
	final public void addHeaderView (View v) {
		throw new RuntimeException("Headers are not supported with TouchListView");
	}
  
  @Override
  final public void addFooterView(View v, Object data, boolean isSelectable) {
    if (mRemoveMode == SLIDE_LEFT || mRemoveMode == SLIDE_RIGHT) {
      throw new RuntimeException("Footers are not supported with TouchListView in conjunction with remove_mode");
    }
  }
  
  @Override
  final public void addFooterView(View v) {
    if (mRemoveMode == SLIDE_LEFT || mRemoveMode == SLIDE_RIGHT) {
      throw new RuntimeException("Footers are not supported with TouchListView in conjunction with remove_mode");
    }
  }
    
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
        if( mRemoveListener != null && mGestureDetector == null ) {
            if( mRemoveMode == FLING ) {
                mGestureDetector = new GestureDetector( getContext(), new SimpleOnGestureListener() {
                    @Override
                    public boolean onFling( MotionEvent e1, MotionEvent e2, float velocityX, float velocityY ) {
                        if( mDragView != null ) {
                            if( velocityX > 1000 ) {
                                Rect r = mTempRect;
                                mDragView.getDrawingRect( r );
                                if( e2.getX() > r.right * 2 / 3 ) {
                                    // fast fling right with release near the
                                    // right edge of the screen
                                    stopDragging();
                                    mRemoveListener.remove( mFirstDragPos );
                                    unExpandViews( true );
                                }
                            }
                            // flinging while dragging should have no effect
                            return true;
                        }
                        return false;
                    }
                } );
            }
        }
        if( mDragListener != null || mDropListener != null ) {
            switch( ev.getAction() ) {
            case MotionEvent.ACTION_DOWN:
                int x = (int)ev.getX();
                int y = (int)ev.getY();
                int itemnum = pointToPosition( x, y );
                if( itemnum == AdapterView.INVALID_POSITION ) {
                    break;
                }

                View item = (View)getChildAt( itemnum - getFirstVisiblePosition() );

                if( isDraggableRow( item ) ) {
                    mDragPoint = y - item.getTop();
                    mCoordOffset = ( (int)ev.getRawY() ) - y;
                    View dragger = item.findViewById( grabberId );
                    Rect r = mTempRect;
                    // dragger.getDrawingRect(r);

                    r.left = dragger.getLeft();
                    r.right = dragger.getRight();
                    r.top = dragger.getTop();
                    r.bottom = dragger.getBottom();

                    if( ( r.left < x ) && ( x < r.right ) ) {
                        item.setDrawingCacheEnabled( true );
                        // Create a copy of the drawing cache so that it does
                        // not get recycled
                        // by the framework when the list tries to clean up
                        // memory
                        Bitmap bitmap = Bitmap.createBitmap( item.getDrawingCache() );

                        Rect listBounds = new Rect();

                        getGlobalVisibleRect( listBounds, null );

                        startDragging( bitmap, listBounds.left, y );
                        mDragPos = itemnum;
                        mFirstDragPos = mDragPos;
                        mHeight = getHeight();
                        int touchSlop = mTouchSlop;
                        mUpperBound = Math.min( y - touchSlop, mHeight / 3 );
                        mLowerBound = Math.max( y + touchSlop, mHeight * 2 / 3 );
                        return false;
                    }

                    mDragView = null;
                }

                break;
            }
        }
        return super.onInterceptTouchEvent( ev );
	}
	
	protected boolean isDraggableRow(View view) {
	    return(view.findViewById(grabberId)!=null);
	}
	
	/*
	 * pointToPosition() doesn't consider invisible views, but we
	 * need to, so implement a slightly different version.
	 */
	private int myPointToPosition(int x, int y) {
        Rect frame = mTempRect;
        final int count = getChildCount();
        for( int i = count - 1; i >= 0; i-- ) {
            final View child = getChildAt( i );
            child.getHitRect( frame );
            if( frame.contains( x, y ) ) {
                return getFirstVisiblePosition() + i;
            }
        }
        return INVALID_POSITION;
	}
	
	private int getItemForPosition(int y) {
        int adjustedy = y - mDragPoint - 32;
        int pos = myPointToPosition( 0, adjustedy );
        if( pos >= 0 ) {
            if( pos <= mFirstDragPos ) {
                pos += 1;
            }
        } else if( adjustedy < 0 ) {
            pos = 0;
        }
        return pos;
	}
	
	private void adjustScrollBounds(int y) {
        if( y >= mHeight / 3 ) {
            mUpperBound = mHeight / 3;
        }
        if( y <= mHeight * 2 / 3 ) {
            mLowerBound = mHeight * 2 / 3;
        }
	}

	/*
	 * Restore size and visibility for all listitems
	 */
	private void unExpandViews(boolean deletion) {
        for( int i = 0;; i++ ) {
            View v = getChildAt( i );
            if( v == null ) {
                if( deletion ) {
                    // HACK force update of mItemCount
                    int position = getFirstVisiblePosition();
                    int y = getChildAt( 0 ).getTop();
                    setAdapter( getAdapter() );
                    setSelectionFromTop( position, y );
                    // end hack
                }
                layoutChildren(); // force children to be recreated where needed
                v = getChildAt( i );
                if( v == null ) {
                    break;
                }
            }

            if( isDraggableRow( v ) ) {
                ViewGroup.LayoutParams params = v.getLayoutParams();
                params.height = mItemHeightNormal;
                v.setLayoutParams( params );
                v.setVisibility( View.VISIBLE );
            }
        }
	}
	
	/* Adjust visibility and size to make it appear as though
	 * an item is being dragged around and other items are making
	 * room for it:
	 * If dropping the item would result in it still being in the
	 * same place, then make the dragged listitem's size normal,
	 * but make the item invisible.
	 * Otherwise, if the dragged listitem is still on screen, make
	 * it as small as possible and expand the item below the insert
	 * point.
	 * If the dragged item is not on screen, only expand the item
	 * below the current insertpoint.
	 */
	private void doExpansion() {
        int childnum = mDragPos - getFirstVisiblePosition();
        if( mDragPos > mFirstDragPos ) {
            childnum++;
        }

        View first = getChildAt( mFirstDragPos - getFirstVisiblePosition() );

        for( int i = 0;; i++ ) {
            View vv = getChildAt( i );
            if( vv == null ) {
                break;
            }
            int height = mItemHeightNormal;
            int visibility = View.VISIBLE;
            if( vv.equals( first ) ) {
                // processing the item that is being dragged
                if( mDragPos == mFirstDragPos ) {
                    // hovering over the original location
                    visibility = View.INVISIBLE;
                } else {
                    // not hovering over it
                    height = 1;
                }
            } else if( i == childnum ) {
                if( mDragPos < getCount() - 1 ) {
                    height = mItemHeightExpanded;
                }
            }

            if( isDraggableRow( vv ) ) {
                ViewGroup.LayoutParams params = vv.getLayoutParams();
                params.height = height;
                vv.setLayoutParams( params );
                vv.setVisibility( visibility );
            }
        }
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent ev) {
        if( mGestureDetector != null ) {
            mGestureDetector.onTouchEvent( ev );
        }
        if( ( mDragListener != null || mDropListener != null ) && mDragView != null ) {
            int action = ev.getAction();
            switch( action ) {
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                Rect r = mTempRect;
                mDragView.getDrawingRect( r );
                stopDragging();

                if( mRemoveMode == SLIDE_RIGHT && ev.getX() > r.left + ( r.width() * 3 / 4 ) ) {
                    if( mRemoveListener != null ) {
                        mRemoveListener.remove( mFirstDragPos );
                    }
                    unExpandViews( true );
                } else if( mRemoveMode == SLIDE_LEFT && ev.getX() < r.left + ( r.width() / 4 ) ) {
                    if( mRemoveListener != null ) {
                        mRemoveListener.remove( mFirstDragPos );
                    }
                    unExpandViews( true );
                } else {
                    if( mDropListener != null && mDragPos >= 0 && mDragPos < getCount() ) {
                        mDropListener.drop( mFirstDragPos, mDragPos );
                    }
                    unExpandViews( false );
                }
                break;

            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
                int x = (int)ev.getX();
                int y = (int)ev.getY();
                dragView( x, y );
                int itemnum = getItemForPosition( y );
                if( itemnum >= 0 ) {
                    if( action == MotionEvent.ACTION_DOWN || itemnum != mDragPos ) {
                        if( mDragListener != null ) {
                            mDragListener.drag( mDragPos, itemnum );
                        }
                        mDragPos = itemnum;
                        doExpansion();
                    }
                    int speed = 0;
                    adjustScrollBounds( y );
                    if( y > mLowerBound ) {
                        // scroll the list up a bit
                        speed = y > ( mHeight + mLowerBound ) / 2 ? 16 : 4;
                    } else if( y < mUpperBound ) {
                        // scroll the list down a bit
                        speed = y < mUpperBound / 2 ? -16 : -4;
                    }
                    if( speed != 0 ) {
                        int ref = pointToPosition( 0, mHeight / 2 );
                        if( ref == AdapterView.INVALID_POSITION ) {
                            // we hit a divider or an invisible view, check
                            // somewhere else
                            ref = pointToPosition( 0, mHeight / 2 + getDividerHeight() + 64 );
                        }
                        View v = getChildAt( ref - getFirstVisiblePosition() );
                        if( v != null ) {
                            int pos = v.getTop();
                            setSelectionFromTop( ref, pos - speed );
                        }
                    }
                }
                break;
            }
            return true;
        }
        return super.onTouchEvent( ev );
	}
	
	private void startDragging(Bitmap bm, int x, int y) {
        stopDragging();

        mWindowParams = new WindowManager.LayoutParams();
        mWindowParams.gravity = Gravity.TOP | Gravity.LEFT;
        mWindowParams.x = x;
        mWindowParams.y = y - mDragPoint + mCoordOffset;

        mWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
        mWindowParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
        mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
                | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
        mWindowParams.format = PixelFormat.TRANSLUCENT;
        mWindowParams.windowAnimations = 0;

        ImageView v = new ImageView( getContext() );
        // int backGroundColor =
        // getContext().getResources().getColor(R.color.dragndrop_background);
        v.setBackgroundColor( dragndropBackgroundColor );
        v.setImageBitmap( bm );
        mDragBitmap = bm;

        mWindowManager = (WindowManager)getContext().getSystemService( "window" );
        mWindowManager.addView( v, mWindowParams );
        mDragView = v;
	}
	
	private void dragView(int x, int y) {
        float alpha = 1.0f;
        int width = mDragView.getWidth();

        if( mRemoveMode == SLIDE_RIGHT ) {
            if( x > width / 2 ) {
                alpha = ( (float)( width - x ) ) / ( width / 2 );
            }
            mWindowParams.alpha = alpha;
        } else if( mRemoveMode == SLIDE_LEFT ) {
            if( x < width / 2 ) {
                alpha = ( (float)x ) / ( width / 2 );
            }
            mWindowParams.alpha = alpha;
        }
        mWindowParams.y = y - mDragPoint + mCoordOffset;
        mWindowManager.updateViewLayout( mDragView, mWindowParams );
	}
	
	private void stopDragging() {
        if( mDragView != null ) {
            WindowManager wm = (WindowManager)getContext().getSystemService( "window" );
            wm.removeView( mDragView );
            mDragView.setImageDrawable( null );
            mDragView = null;
        }
        if( mDragBitmap != null ) {
            mDragBitmap.recycle();
            mDragBitmap = null;
        }
	}
	
	public void setDragListener(DragListener l) {
			mDragListener = l;
	}
	
	public void setDropListener(DropListener l) {
			mDropListener = l;
	}
	
	public void setRemoveListener(RemoveListener l) {
			mRemoveListener = l;
	}

	public interface DragListener {
			void drag(int from, int to);
	}
	public interface DropListener {
			void drop(int from, int to);
	}
	public interface RemoveListener {
			void remove(int which);
	}
}
